/** @module GUI */
import {
	document
} from "../../DHTML"
import Controller from './Controller';
import BooleanController from './BooleanController';
import ColorController from './ColorController';
import FunctionController from './FunctionController';
import NumberController from './NumberController';
import OptionController from './OptionController';
import StringController from './StringController';

export default class GUI {

	/**
	 * Creates a panel that holds controllers.
	 * @example
	 * new GUI();
	 * new GUI( { container: document.getElementById( 'custom' ) } );
	 *
	 * @param {object} [options]
	 * @param {boolean} [options.autoPlace=true]
	 * Adds the GUI to `document.body` and fixes it to the top right of the page.
	 *
	 * @param {HTMLElement} [options.container]
	 * Adds the GUI to this DOM element. Overrides `autoPlace`.
	 *
	 * @param {number} [options.width=245]
	 * Width of the GUI in pixels, usually set when name labels become too long. Note that you can make
	 * name labels wider in CSS with `.lil‑gui { ‑‑name‑width: 55% }`
	 *
	 * @param {string} [options.title=Controls]
	 * Name to display in the title bar.
	 *
	 * @param {boolean} [options.injectStyles=true]
	 * Injects the default stylesheet into the page if this is the first GUI.
	 * Pass `false` to use your own stylesheet.
	 *
	 * @param {number} [options.touchStyles=true]
	 * Makes controllers larger on touch devices. Pass `false` to disable touch styles.
	 *
	 * @param {GUI} [options.parent]
	 * Adds this GUI as a child in another GUI. Usually this is done for you by `addFolder()`.
	 *
	 */
	constructor(UC, {
		parent,
		autoPlace = parent === undefined,
		container,
		width = 310,
		title = 'Controls'
	} = {}) {
		this.UC = UC
		if (parent != null) {
			this.PATH = `${parent.PATH}children[${parent.children.length}].`
		} else {
			this.PATH = ""
		}
		if (parent != null) {
			var children = this._findData(`${parent.PATH}children`)
			children.push({
				type: "GUI",
				show:true,
				children: []
			})
			var data = {}
			data[`${parent.PATH}children`] = children
			this.UC.setData(data)
		}
		////////////////////////////////////////////
		/**
		 * The GUI containing this folder, or `undefined` if this is the root GUI.
		 * @type {GUI}
		 */
		this.parent = parent;

		/**
		 * The top level GUI containing this folder, or `this` if this is the root GUI.
		 * @type {GUI}
		 */
		this.root = parent ? parent.root : this;

		/**
		 * The list of controllers and folders contained by this GUI.
		 * @type {Array<GUI|Controller>}
		 */
		this.children = [];

		/**
		 * The list of controllers contained by this GUI.
		 * @type {Array<Controller>}
		 */
		this.controllers = [];

		/**
		 * The list of folders contained by this GUI.
		 * @type {Array<GUI>}
		 */
		this.folders = [];

		/**
		 * Used to determine if the GUI is closed. Use `gui.open()` or `gui.close()` to change this.
		 * @type {boolean}
		 */
		this._closed = false;

		/**
		 * Used to determine if the GUI is hidden. Use `gui.show()` or `gui.hide()` to change this.
		 * @type {boolean}
		 */
		this._hidden = false;

		/**
		 * The outermost container element.
		 * @type {HTMLElement}
		 */
		this.domElement = document.createElement('div');
		this.domElement.classList.add('lil-gui');

		/**
		 * The DOM element that contains the title.
		 * @type {HTMLElement}
		 */
		this.$title = document.createElement('div');
		this.$title.classList.add('title');
		this.$title.setAttribute('role', 'button');
		this.$title.setAttribute('aria-expanded', true);
		this.$title.setAttribute('tabindex', 0);

		this.$title.addEventListener('click', () => this.openAnimated(this._closed));
		this.$title.addEventListener('keydown', e => {
			if (e.code === 'Enter' || e.code === 'Space') {
				e.preventDefault();
				this.$title.click();
			}
		});

		// enables :active pseudo class on mobile
		this.$title.addEventListener('touchstart', () => {}, {
			passive: true
		});

		/**
		 * The DOM element that contains children.
		 * @type {HTMLElement}
		 */
		this.$children = document.createElement('div');
		this.$children.classList.add('children');

		this.domElement.appendChild(this.$title);
		this.domElement.appendChild(this.$children);

		this.title(title);

		if (this.parent) {

			this.parent.children.push(this);
			this.parent.folders.push(this);

			this.parent.$children.appendChild(this.domElement);

			// Stop the constructor early, everything onward only applies to root GUI's
			return;

		}

		this.domElement.classList.add('root');

		if (container) {

			container.appendChild(this.domElement);

		} else if (autoPlace) {

			this.domElement.classList.add('autoPlace');
			document.body.appendChild(this.domElement);

		}

		if (width) {
			//this.domElement.style.setProperty('--width', width + 'px');
			this.UC.setData({
				width
			})
		}

		// Don't fire global key events while typing in the GUI:
		this.domElement.addEventListener('keydown', e => e.stopPropagation());
		this.domElement.addEventListener('keyup', e => e.stopPropagation());

	}

	/**
	 * Adds a controller to the GUI, inferring controller type using the `typeof` operator.
	 * @example
	 * gui.add( object, 'property' );
	 * gui.add( object, 'number', 0, 100, 1 );
	 * gui.add( object, 'options', [ 1, 2, 3 ] );
	 *
	 * @param {object} object The object the controller will modify.
	 * @param {string} property Name of the property to control.
	 * @param {number|object|Array} [$1] Minimum value for number controllers, or the set of
	 * selectable values for a dropdown.
	 * @param {number} [max] Maximum value for number controllers.
	 * @param {number} [step] Step value for number controllers.
	 * @returns {Controller}
	 */
	add(object, property, $1, max, step) {

		if (Object($1) === $1) {

			return new OptionController(this, object, property, $1);

		}

		const initialValue = object[property];

		switch (typeof initialValue) {

			case 'number':

				return new NumberController(this, object, property, $1, max, step);

			case 'boolean':

				return new BooleanController(this, object, property);

			case 'string':

				return new StringController(this, object, property);

			case 'function':

				return new FunctionController(this, object, property);

		}

		// eslint-disable-next-line no-console
		console.error(`gui.add failed
	property:`, property, `
	object:`, object, `
	value:`, initialValue);

	}

	/**
	 * Adds a color controller to the GUI.
	 * @example
	 * params = {
	 * 	cssColor: '#ff00ff',
	 * 	rgbColor: { r: 0, g: 0.2, b: 0.4 },
	 * 	customRange: [ 0, 127, 255 ],
	 * };
	 *
	 * gui.addColor( params, 'cssColor' );
	 * gui.addColor( params, 'rgbColor' );
	 * gui.addColor( params, 'customRange', 255 );
	 *
	 * @param {object} object The object the controller will modify.
	 * @param {string} property Name of the property to control.
	 * @param {number} rgbScale Maximum value for a color channel when using an RGB color. You may
	 * need to set this to 255 if your colors are too bright.
	 * @returns {Controller}
	 */
	addColor(object, property, rgbScale = 1) {
		return new ColorController(this, object, property, rgbScale);
	}

	/**
	 * Adds a folder to the GUI, which is just another GUI. This method returns
	 * the nested GUI so you can add controllers to it.
	 * @example
	 * const folder = gui.addFolder( 'Position' );
	 * folder.add( position, 'x' );
	 * folder.add( position, 'y' );
	 * folder.add( position, 'z' );
	 *
	 * @param {string} title Name to display in the folder's title bar.
	 * @returns {GUI}
	 */
	addFolder(title) {
		return new GUI(this.UC, {
			parent: this,
			title
		});
	}

	/**
	 * Recalls values that were saved with `gui.save()`.
	 * @param {object} obj
	 * @param {boolean} recursive Pass false to exclude folders descending from this GUI.
	 * @returns {this}
	 */
	load(obj, recursive = true) {

		if (obj.controllers) {

			this.controllers.forEach(c => {

				if (c instanceof FunctionController) return;

				if (c._name in obj.controllers) {
					c.load(obj.controllers[c._name]);
				}

			});

		}

		if (recursive && obj.folders) {

			this.folders.forEach(f => {

				if (f._title in obj.folders) {
					f.load(obj.folders[f._title]);
				}

			});

		}

		return this;

	}

	/**
	 * Returns an object mapping controller names to values. The object can be passed to `gui.load()` to
	 * recall these values.
	 * @example
	 * {
	 * 	controllers: {
	 * 		prop1: 1,
	 * 		prop2: 'value',
	 * 		...
	 * 	},
	 * 	folders: {
	 * 		folderName1: { controllers, folders },
	 * 		folderName2: { controllers, folders }
	 * 		...
	 * 	}
	 * }
	 *
	 * @param {boolean} recursive Pass false to exclude folders descending from this GUI.
	 * @returns {object}
	 */
	save(recursive = true) {

		const obj = {
			controllers: {},
			folders: {}
		};

		this.controllers.forEach(c => {

			if (c instanceof FunctionController) return;

			if (c._name in obj.controllers) {
				throw new Error(`Cannot save GUI with duplicate property "${c._name}"`);
			}

			obj.controllers[c._name] = c.save();

		});

		if (recursive) {

			this.folders.forEach(f => {

				if (f._title in obj.folders) {
					throw new Error(`Cannot save GUI with duplicate folder "${f._title}"`);
				}

				obj.folders[f._title] = f.save();

			});

		}

		return obj;

	}

	/**
	 * Opens a GUI or folder. GUI and folders are open by default.
	 * @param {boolean} open Pass false to close
	 * @returns {this}
	 * @example
	 * gui.open(); // open
	 * gui.open( false ); // close
	 * gui.open( gui._closed ); // toggle
	 */
	open(open = true) {

		this._closed = !open;

		this.$title.setAttribute('aria-expanded', !this._closed);
		this.domElement.classList.toggle('closed', this._closed);

		return this;

	}

	/**
	 * Closes the GUI.
	 * @returns {this}
	 */
	close() {
		return this.open(false);
	}

	/**
	 * Shows the GUI after it's been hidden.
	 * @param {boolean} show
	 * @returns {this}
	 * @example
	 * gui.show();
	 * gui.show( false ); // hide
	 * gui.show( gui._hidden ); // toggle
	 */
	show(show = true) {

		this._hidden = !show;

		this.domElement.style.display = this._hidden ? 'none' : '';
		this._render("show",show)

		return this;

	}

	/**
	 * Hides the GUI.
	 * @returns {this}
	 */
	hide() {
		return this.show(false);
	}

	openAnimated(open = true) {

		// set state immediately
		this._closed = !open;

		this.$title.setAttribute('aria-expanded', !this._closed);

		// wait for next frame to measure $children
		requestAnimationFrame(() => {

			// explicitly set initial height for transition
			const initialHeight = this.$children.clientHeight;
			this.$children.style.height = initialHeight + 'px';

			this.domElement.classList.add('transition');

			const onTransitionEnd = e => {
				if (e.target !== this.$children) return;
				this.$children.style.height = '';
				this.domElement.classList.remove('transition');
				this.$children.removeEventListener('transitionend', onTransitionEnd);
			};

			this.$children.addEventListener('transitionend', onTransitionEnd);

			// todo: this is wrong if children's scrollHeight makes for a gui taller than maxHeight
			const targetHeight = !open ? 0 : this.$children.scrollHeight;

			this.domElement.classList.toggle('closed', !open);

			requestAnimationFrame(() => {
				this.$children.style.height = targetHeight + 'px';
			});

		});

		return this;

	}

	/**
	 * Change the title of this GUI.
	 * @param {string} title
	 * @returns {this}
	 */
	title(title) {
		/**
		 * Current title of the GUI. Use `gui.title( 'Title' )` to modify this value.
		 * @type {string}
		 */
		this._title = title;
		this.$title.innerHTML = title;

		this._render("title", title)
		return this;
	}

	/**
	 * Resets all controllers to their initial values.
	 * @param {boolean} recursive Pass false to exclude folders descending from this GUI.
	 * @returns {this}
	 */
	reset(recursive = true) {
		const controllers = recursive ? this.controllersRecursive() : this.controllers;
		controllers.forEach(c => c.reset());
		return this;
	}

	/**
	 * Pass a function to be called whenever a controller in this GUI changes.
	 * @param {function({object:object, property:string, value:any, controller:Controller})} callback
	 * @returns {this}
	 * @example
	 * gui.onChange( event => {
	 * 	event.object     // object that was modified
	 * 	event.property   // string, name of property
	 * 	event.value      // new value of controller
	 * 	event.controller // controller that was modified
	 * } );
	 */
	onChange(callback) {
		/**
		 * Used to access the function bound to `onChange` events. Don't modify this value
		 * directly. Use the `gui.onChange( callback )` method instead.
		 * @type {Function}
		 */
		this._onChange = callback;
		return this;
	}

	_callOnChange(controller) {

		if (this.parent) {
			this.parent._callOnChange(controller);
		}

		if (this._onChange !== undefined) {
			this._onChange.call(this, {
				object: controller.object,
				property: controller.property,
				value: controller.getValue(),
				controller
			});
		}
	}

	/**
	 * Pass a function to be called whenever a controller in this GUI has finished changing.
	 * @param {function({object:object, property:string, value:any, controller:Controller})} callback
	 * @returns {this}
	 * @example
	 * gui.onFinishChange( event => {
	 * 	event.object     // object that was modified
	 * 	event.property   // string, name of property
	 * 	event.value      // new value of controller
	 * 	event.controller // controller that was modified
	 * } );
	 */
	onFinishChange(callback) {
		/**
		 * Used to access the function bound to `onFinishChange` events. Don't modify this value
		 * directly. Use the `gui.onFinishChange( callback )` method instead.
		 * @type {Function}
		 */
		this._onFinishChange = callback;
		return this;
	}

	_callOnFinishChange(controller) {

		if (this.parent) {
			this.parent._callOnFinishChange(controller);
		}

		if (this._onFinishChange !== undefined) {
			this._onFinishChange.call(this, {
				object: controller.object,
				property: controller.property,
				value: controller.getValue(),
				controller
			});
		}
	}

	/**
	 * Destroys all DOM elements and event listeners associated with this GUI
	 */
	destroy() {

		if (this.parent) {
			this.parent.children.splice(this.parent.children.indexOf(this), 1);
			this.parent.folders.splice(this.parent.folders.indexOf(this), 1);
		}

		if (this.domElement.parentElement) {
			this.domElement.parentElement.removeChild(this.domElement);
		}

		Array.from(this.children).forEach(c => c.destroy());

	}

	/**
	 * Returns an array of controllers contained by this GUI and its descendents.
	 * @returns {Controller[]}
	 */
	controllersRecursive() {
		let controllers = Array.from(this.controllers);
		this.folders.forEach(f => {
			controllers = controllers.concat(f.controllersRecursive());
		});
		return controllers;
	}

	/**
	 * Returns an array of folders contained by this GUI and its descendents.
	 * @returns {GUI[]}
	 */
	foldersRecursive() {
		let folders = Array.from(this.folders);
		this.folders.forEach(f => {
			folders = folders.concat(f.foldersRecursive());
		});
		return folders;
	}
	_render(key_, value) {
		const data = {}
		var key = `${this.PATH}${key_}`
		data[key] = value
		this.UC.setData(data)
	}
	_findData(path) {
		var array = path.split(".")
		var data = this.UC.data
		for (const item of array) {
			var key, index
			var p = item.indexOf("[")
			var q = item.indexOf("]")
			if (q >= 0) {
				var key = item.substr(0, p)
				var index = item.substring(p + 1, q)
				data = data[key][index]
			} else {
				data = data[item]
			}
		}
		return data
	}
	_findNode(path) {
		var array = path.split(".")
		var data = this
		for (const item of array) {
			var key, index
			var p = item.indexOf("[")
			var q = item.indexOf("]")

			var index = item.substring(p + 1, q)
			data = data.children[index]
		}
		return data
	}
}
